﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows.Media;

namespace Petra.Gis.Geometry.Image.Tiff
{
  public class TiffMetadata //: System.Windows.Freezable
  {

    bool _loadBitonalAsPaletted8bpp;
    bool _saveBitonalAsPaletted8bpp;
    ImageFileDirectory _ifd;



    internal TiffMetadata(ImageFileDirectory ifd)
    {
      _ifd = ifd;
    }




    //protected override System.Windows.Freezable CreateInstanceCore()
    //{
    //  return new TiffMetadata(_ifd.Clone());
    //}


    #region public properties


    /// <summary>
    /// returns image file directory of current frame/page
    /// </summary>
    public ImageFileDirectory Ifd
    {
      get { return _ifd; }
    }



    public TiffPixelFormat PixelFormat
    {
      get { return getPixelFormat(); }
    }


    public List<Color> ColorMap
    {
      get 
      {

        Tag tag = _ifd.GetTag(Tag.TagId.COLORMAP);

        if ((tag.Count % 3) != 0)
          throw new InvalidFileStructureException("invalid color map in tiff");

        List<Color> result = new List<Color>();

        int numEntries = tag.Count / 3;
        int offsetToNextColor = numEntries * 2; //todo: change to tag data type, now fixed 2 bytes = 16 bits

        for (int i = 0; i < numEntries; i++)
        { 
          result.Add(Color.FromRgb(
              (byte)(tag.getElementAsInt(i) >> 8),
              (byte)(tag.getElementAsInt(offsetToNextColor + i) >> 8),
              (byte)(tag.getElementAsInt(offsetToNextColor + offsetToNextColor + i) >> 8)
              ));
        }

        return result;
      }
    }


    /// <summary>
    /// returns info about block configuration
    /// data per block is either
    /// RGBRGBRGBRGB.....
    /// or
    /// REDBLOCK,GREENBLOCK,BLUEBLOCK
    /// </summary>
    public PlanarConfigurationType PlanarConfiguration
    {
      get 
      {
        try { _ifd.GetTag(Tag.TagId.PLANARCONFIG); }
        catch { return PlanarConfigurationType.Contignous; } //default is always PlanarConfigurationType.Contignous

        //else PLANARCONFIG exists return its value
        return (PlanarConfigurationType)_ifd.GetTag(Tag.TagId.PLANARCONFIG).TagData[0];
      }
    }


    /// <summary>
    /// Returns information on the person who created the image
    ///(as stored in tag TagId.ARTIST).
    /// </summary>
    public string Artist
    {
      get { return (string)_ifd.GetTag(Tag.TagId.ARTIST).TagData[0]; }
    }



    /// <summary>
    /// Returns the number of bits per pixel (not including transparency information).
    /// </summary>
    public int BitsPerBand
    {
      get { return (int)_ifd.GetTag(Tag.TagId.BITSPERSAMPLE).TagData[0]; }
    }


    /// <summary>
    /// Returns the number of bits per pixel (not including transparency information).
    /// </summary>
    public int[] BitsPerSamples
    {
      get {
        try { _ifd.GetTag(Tag.TagId.BITSPERSAMPLE); }
        catch { return new int[] {1}; } //if this tag not exists, treat as bitonal (default)

        //else resolve by tag
        int[] result = new int[_ifd.GetTag(Tag.TagId.BITSPERSAMPLE).Count];
        for (int i = 0; i < result.Length; i++)
        {
          result[i] = System.Convert.ToInt32(_ifd.GetTag(Tag.TagId.BITSPERSAMPLE).TagData[i]);
        }
        return result;
      }
    }




    ///<summary>
    /// Returns the compression method applied to data in current frame/page
    ///</summary>
    public CompressionType Compression
    {
      get { return (CompressionType)_ifd.GetTag(Tag.TagId.COMPRESSION).TagData[0]; }
    }



    /// <summary>
    /// returns image copyright tag info
    /// </summary>
    public string Copyright
    {
      get { return (string)_ifd.GetTag(Tag.TagId.COPYRIGHT).TagData[0]; }
    }



    /// <summary>
    /// returns date time tag informations
    /// </summary>
    public string DateTime
    {
      get { return (string)_ifd.GetTag(Tag.TagId.DATETIME).TagData[0]; }
    }



    public string HostComputer
    {
      get { return (string)_ifd.GetTag(Tag.TagId.HOSTCOMPUTER).TagData[0]; }
    }



    public string ImageDescription
    {
      get { return (string)_ifd.GetTag(Tag.TagId.IMAGEDESCRIPTION).TagData[0]; }
    }

    /// <summary>
    /// returns photometric interpretation of current image
    /// </summary>
    public PhotometricType PhotometricInterpretation
    {
      //mandatory field !! must exists
      get { return (PhotometricType)_ifd.GetTag(Tag.TagId.PHOTOMETRIC).TagData[0]; }
    }



    /// <summary>
    /// returns predictor
    /// </summary>
    public int Predictor
    {
      get { return (int)_ifd.GetTag(Tag.TagId.PREDICTOR).TagData[0]; }
    }



    /// <summary>
    /// returns value of tag T4OPTIONS for fax compressed images
    /// </summary>
    public int T4Options
    {
      get { return (int)_ifd.GetTag(Tag.TagId.T4OPTIONS).TagData[0]; }
    }




    /// <summary>
    /// returns value of tag T6OPTIONS for fax compressed images
    /// </summary>
    public TiffFaxT6Option T6Options
    {
      get 
      {
        return (TiffFaxT6Option)_ifd.GetTag(Tag.TagId.T6OPTIONS).TagData[0]; 
      }
    }


    public FillOrderType FillOrder
    {
      get 
      {
        try { _ifd.GetTag(Tag.TagId.FILLORDER); }
        catch { return FillOrderType.MSB2LSB; }

        //else get real fillOrder from data
        return (FillOrderType)_ifd.GetTag(Tag.TagId.FILLORDER).TagData[0];
      }
    }





    /// <summary>
    /// for specific purposes is still keept original tag value SAMPLESPERPIXEL
    /// </summary>
    public int SamplesPerPixel
    {
      get 
      {
        try {_ifd.GetTag(Tag.TagId.SAMPLESPERPIXEL); }
        catch { return 1; }

        //else return value
        return (int)_ifd.GetTag(Tag.TagId.SAMPLESPERPIXEL).TagData[0]; 
      
      }
    }



    public string Software
    {
      get { return (string)_ifd.GetTag(Tag.TagId.SOFTWARE).TagData[0]; }
    }



    /// <summary>
    /// for specific purposes is still keept original tag value ROWSPERSTRIP
    /// </summary>
    public int RowsPerStrip
    {
      get { return (int)_ifd.GetTag(Tag.TagId.ROWSPERSTRIP).TagData[0]; }
    }



    public ArrayList StripOffsets
    {
      get { return _ifd.GetTag(Tag.TagId.STRIPOFFSETS).TagData; }
    }



    /// <summary>
    /// for specific purposes is still keept original tag value STRIPOFFSETS
    /// </summary>
    public int NumOfStrips
    {
      get { return (int)_ifd.GetTag(Tag.TagId.STRIPOFFSETS).Count; }
    }



    /// <summary>
    /// for specific purposes is still keept original tag value TILEOFFSETS
    /// </summary>
    public int NumTiles
    {
      get { return (int)_ifd.GetTag(Tag.TagId.TILEOFFSETS).Count; }
    }


    /// <summary>
    /// return number of block in horizontal direction in image (computed value from tags)
    /// </summary>
    public int NumOfHorizontalBlocks
    {
      get { return getNumHorizontalTiles(); }
    }




    /// <summary>
    /// return number of block in vertical direction in image (computed value from tags)
    /// </summary>
    public int NumOfVerticalBlocks
    {
      get { return getNumVerticalTiles(); }
    }



    /// <summary>
    /// Returns <code>true</code> if current Frame.page
    /// is stored as tiles, 
    /// <code>false</code> otherwise is returned when frame is stripped
    /// </summary>
    public bool IsTiled
    {
      get
      {
        try { _ifd.GetTag(Tag.TagId.TILEOFFSETS); }
        catch
        {
          //otherview must be stripped, so try get STRIPOFFSETS tag...
          _ifd.GetTag(Tag.TagId.STRIPOFFSETS);
          return false;
        }
        return true;
      }
    }



    /// <summary>
    /// returns true when image is colormap (color table) based 
    /// </summary>
    /// <returns></returns>
    public bool IsColorMapImage()
    {
      if (((PhotometricType)_ifd.GetTag(Tag.TagId.PHOTOMETRIC).TagData[0]) != PhotometricType.Paletted)
      {
        //try if color map exists
        _ifd.GetTag(Tag.TagId.COLORMAP);
        return true;
      }
      else
        return false;
    }



    /// <summary>
    /// returns total number of blocks in current frame/page
    /// </summary>
    public int NumBlocks
    {
      get
      {
        if (this.IsTiled)
          return this.NumTiles;
        else
          return this.NumOfStrips;
      }
    }



    /// <summary>
    /// returns implicit height of block.
    /// Please note, than last blocks in image (last row) might vary from this size
    /// and might be smaller
    /// </summary>
    public int BlockHeight
    {
      get
      {
        if (this.IsTiled)
          return (int)_ifd.GetTag(Tag.TagId.TILELENGTH).TagData[0];
        else
          return (int)_ifd.GetTag(Tag.TagId.ROWSPERSTRIP).TagData[0];
      }
    }



    /// <summary>
    /// returns implicit width of block.
    /// Please note, than last blocks in image (last column) might vary from this size
    /// and might be smaller
    /// </summary>
    public int BlockWidth
    {
      get
      {
        if (this.IsTiled)
          return (int)_ifd.GetTag(Tag.TagId.TILEWIDTH).TagData[0];
        else
          return this.Width;
      }
    }



    /// <summary>
    /// special metadata - returns scanner model
    /// </summary>
    public string Model
    {
      get { return (string)_ifd.GetTag(Tag.TagId.MODEL).TagData[0]; }
    }


    /// <summary>
    /// returns height of current frame/page in pixels(samples)
    /// </summary>
    public int Height
    {
      get { return (int)_ifd.GetTag(Tag.TagId.IMAGELENGTH).TagData[0]; }
    }



    /// <summary>
    /// returns width of current frame/page in pixels(samples)
    /// </summary>
    public int Width
    {
      get { return (int)_ifd.GetTag(Tag.TagId.IMAGEWIDTH).TagData[0]; }
    }




    /// <summary>
    /// returns tiff file offset of current block (tile or stripe)
    /// </summary>
    public long[] BlockOffsets //tile data offset in tiff file
    {
      get
      {
        long[] result = new long[this.NumBlocks];
        for (int i = 0; i < this.NumBlocks; i++)
        {
          if (this.IsTiled)
            result[i] = System.Convert.ToInt64(_ifd.GetTag(Tag.TagId.TILEOFFSETS).TagData[i]);
          else
            result[i] = System.Convert.ToInt64(_ifd.GetTag(Tag.TagId.STRIPOFFSETS).TagData[i]);
        }
        return result;
      }
    }



    public int[] BlockLengths
    {
      get
      {
        int[] result = new int[this.NumBlocks];
        for (int i = 0; i < this.NumBlocks; i++)
        {
          if (this.IsTiled)
            result[i] = System.Convert.ToInt32(_ifd.GetTag(Tag.TagId.TILEBYTECOUNTS).TagData[i]);
          else
            result[i] = System.Convert.ToInt32(_ifd.GetTag(Tag.TagId.STRIPBYTECOUNTS).TagData[i]);
        }
        return result;
      }
    }



    public bool LoadBitonalAsPaletted8bpp
    {
      get { return _loadBitonalAsPaletted8bpp; }
      set { _loadBitonalAsPaletted8bpp = value; }
    }



    /// <summary>
    /// returns System.Windows.Media.PixelFormat by current TiffMetadata TiffPixelFormat
    /// only for currentry supported types
    /// otherwise raises exception
    /// </summary>
    /// <returns></returns>
    public PixelFormat MapPixelFormat()
    {
      if (this.PixelFormat == TiffPixelFormats.Bilevel1bpp && _loadBitonalAsPaletted8bpp == true)
        return PixelFormats.Indexed8;

      else if (this.PixelFormat == TiffPixelFormats.Bilevel1bpp && _loadBitonalAsPaletted8bpp == false)
        return PixelFormats.BlackWhite;

      else if (this.PixelFormat == TiffPixelFormats.InterleavedRgb24bpp ||
          this.PixelFormat == TiffPixelFormats.PlanarYCbCr24bpp ||
          this.PixelFormat == TiffPixelFormats.InterleavedCMYK32bpp
         )
        return PixelFormats.Rgb24;

      else if (this.PixelFormat == TiffPixelFormats.Gray8bpp)
        return PixelFormats.Gray8;

      else
        throw new NotImplementedException("sorry this format is not implemented now");
    }

    #region public properties


    #endregion private helper functions

    /// <summary>
    /// computes number of tiles in tiled image in horizontal direction
    /// if there is one uncomplette tile at end it is included
    /// </summary>
    /// <returns>number of tiles in horizontal direction</returns>
    private int getNumHorizontalTiles()
    {
      int tileWidth = (int)_ifd.GetTag(Tag.TagId.TILEWIDTH).TagData[0];
      int imgWidth = (int)_ifd.GetTag(Tag.TagId.IMAGEWIDTH).TagData[0];

      if (tileWidth < 1 || imgWidth < 1)
      {
        throw new InvalidFileStructureException("Tile and image width must be > 1 ");
      }

      int horizontalTiles = imgWidth / tileWidth;
      if ((imgWidth % tileWidth) != 0)
      {
        horizontalTiles++;
      }

      return horizontalTiles;
    }



    /// <summary>
    /// computes number of tiles in tiled image in vertical direction
    /// if there is one uncomplette tile at end it is included
    /// </summary>
    /// <returns>number of tiles in vertical direction</returns>

    private int getNumVerticalTiles()
    {
      int tileHeight = (int)_ifd.GetTag(Tag.TagId.TILELENGTH).TagData[0];
      int imgHeight = (int)_ifd.GetTag(Tag.TagId.IMAGELENGTH).TagData[0];


      if (tileHeight < 1 || imgHeight < 1)
      {
        throw new InvalidFileStructureException("Tile and image width must be > 1 ");
      }

      int verticalTiles = imgHeight / tileHeight;
      if ((imgHeight % tileHeight) != 0)
      {
        verticalTiles++;
      }

      return verticalTiles;
    }


    private TiffPixelFormat getPixelFormat()
    {

      if(this.SamplesPerPixel != this.BitsPerSamples.Length)
        throw new DataFormatException("invalid data format");

      if (this.PlanarConfiguration == PlanarConfigurationType.Contignous)
      {
        //try {_ifd.GetTag(Tag.TagId.BITSPERSAMPLE); }
        //catch { return TiffPixelFormats.Bilevel1bpp; } //no bitsPerSample defined, must be bilevel image 

        //if bps.count = 1
        if (this.BitsPerSamples.Length == 1)
        {
          //if bpp = 1, bilevel image
          if (this.BitsPerSamples[0] == 1)
            return TiffPixelFormats.Bilevel1bpp;

          switch (this.PhotometricInterpretation)
          {
            case PhotometricType.WhiteIsZero:
            case PhotometricType.BlackIsZero:
              switch (this.BitsPerSamples[0])
              {
                case 4:
                  return TiffPixelFormats.Gray4bpp;
                case 8:
                  return TiffPixelFormats.Gray8bpp;
                case 16:
                  return TiffPixelFormats.Gray16bpp;
                default:
                  throw new UnsupportedTypeException("sorry unsupported");
              }
            case PhotometricType.Paletted:
              switch (this.BitsPerSamples[0])
              {
                case 4:
                  return TiffPixelFormats.Palette4bpp;
                case 8:
                  return TiffPixelFormats.Palette8bpp;
                //case 16:
                //  return TiffPixelFormats.Palette16bpp;
                default:
                  throw new UnsupportedTypeException("sorry unsupported");
              }
            default:
              throw new DataFormatException("invalid data format");

          }
        }
        else //this.BitsPerSamples.Length > 1
        {
          //select schema from photometric
          switch (this.PhotometricInterpretation)
          {
            case PhotometricType.TruecolorRGB:
              if (this.BitsPerSamples.Length < 3)
                throw new InvalidFileStructureException("invalid file");

              switch (this.BitsPerSamples[0])
              {
                case 8:
                  return TiffPixelFormats.InterleavedRgb24bpp;
                case 16:
                  return TiffPixelFormats.InterleavedRgb48bpp;
                default:
                  throw new UnsupportedTypeException("sorry unsupported");
              }
              //break;
            case PhotometricType.TruecolorCMYK:
              if (this.BitsPerSamples.Length < 4)
                throw new InvalidFileStructureException("invalid file");

              switch (this.BitsPerSamples[0])
              {
                case 8:
                  return TiffPixelFormats.InterleavedCMYK32bpp;
                default:
                  throw new UnsupportedTypeException("sorry unsupported");
              }
              //break;
            default:
              throw new NotSupportedException("missing now, sorry");
          }
        }
      }
      else //PlanarConfigurationType.Separate
      {
        if (this.BitsPerSamples.Length == 1)
          throw new InvalidFileStructureException("planar tiff must have multiple bitsPerSamples");

        switch (this.PhotometricInterpretation)
        {
          case PhotometricType.YCbCr:
            if (this.BitsPerSamples.Length < 3)
              throw new InvalidFileStructureException("invalid file");

            switch (this.BitsPerSamples[0])
            {
              case 8:
                return TiffPixelFormats.PlanarYCbCr24bpp;
              default:
                throw new UnsupportedTypeException("sorry missing");
            }
          case PhotometricType.TruecolorCMYK:
            if (this.BitsPerSamples.Length < 4)
              throw new InvalidFileStructureException("invalid file");

            switch (this.BitsPerSamples[0])
            {
              case 8:
                return TiffPixelFormats.PlanarCMYK32bpp;
              default:
                throw new UnsupportedTypeException("sorry missing");
            }

          default:
            throw new NotImplementedException("sory nbot implemented now");
        }

      }
    }

    #endregion private helper functions
  }
}
